
/*
    GraphDoc.m

    The GraphDoc represents an open Graph document.  GraphDoc receives
    messages from the user interface objects and uses an Expression object
    to do calculations and a LineGraph to display the results.

    The GraphDoc class has one slightly odd external dependency.  It requires
    that the delegate of NXApp be able to provide it with a NXStringTable
    (via the stringTable method) that it can use to look up strings that
    are presented to the user.  In this application, the delegate of NXApp is
    always an instance of the GraphApp class.

    If this were a larger program, the NXStringTable needed by this document
    class would probably be in its own nib section, which would be loaded once
    the first time a GraphDoc is created, and then shared among all GraphDocs.

    You may freely copy, distribute, and reuse the code in this example.
    NeXT disclaims any warranty of any kind, expressed or implied, as to its
    fitness for any particular use.
*/

#import "Graph.h"

/* declare methods static to this class */
@interface GraphDoc(GraphDocPrivate)
- _updateGraphVals;
- _updateGraph:(BOOL)finalChange;
- _updateLinks;
- _write:(const char *)filename;
- _read:(const char *)filename;
- (void)_initVariable:(int)index enable:(BOOL)flag value:(float)val;
- _doSaveWithNewName:(BOOL)doNewName retainName:(BOOL)doRetain;
- _writeGraphToPasteboard:(Pasteboard *)pb types:(const char *const *)types num:(int)numTypes;
- (void)_setSliderCell:(SliderCell *)slider value:(float)val;
@end

@implementation GraphDoc

/* variable names must be between A and H */
#define  MIN_CONST	'A'
#define  MAX_CONST	'H'

/* default resolution of a new graph */
#define DEFAULT_RES	30

/* name used for the real document inside a doc wrapper */
#define DOC_NAME "/GraphDoc.xygraph"

static void writeCellFloat(Cell *obj, NXTypedStream *ts);
static void setXRange(Expression *expr, float min, float max);
static NXTypedStream *openDocStream(const char *filename, int mode);
static int removeFile(const char *file);

- init {
  /* the default initialization is to just open a new document */
    return [self initFromFile:NULL];
}

/*
 * Opens a document.  If file is NULL, we open an untitiled document.  We also
 * create a NXDataLinkManager and hook ourselves up as its delegate for doing
 * Object Links.
 */
- initFromFile:(const char *)file {
    int rows, cols;
    int i;
    TextField *aCell;
    char realPath[MAXPATHLEN+1];

    [super init];

  /* load the UI from the nib section and set attributes not available in IB */
    [NXApp loadNibSection:"GraphDoc.nib" owner:self withNames:NO fromZone:[self zone]];
    [variableTexts getNumRows:&rows numCols:&cols];
    for (i = rows; i--; ) {
	aCell = [variableTexts cellAt:i :0];
	[aCell setFloatingPointFormat:YES left:3 right:2];
	[aCell setEnabled:NO];
    }
    [minXText setFloatingPointFormat:YES left:3 right:2];
    [maxXText setFloatingPointFormat:YES left:3 right:2];
    [variableSliders setAutosizeCells:YES];
    [resolutionText setEntryType:NX_POSINTTYPE];

  /* fake a resize so we get the resolution max up to date */
    [self windowDidResize:window];

  /* create an Expression object we will use to evaluate expressions */
    expr = [[Expression allocFromZone:[self zone]] init];

    if (file) {
      /* read existing document */
	if ([self _read:file]) {
	    name = NXCopyStringBufferFromZone(file, [self zone]);
	    if (realpath(name, realPath))
		realName = NXCopyStringBufferFromZone(realPath, [self zone]);
	    [window setTitleAsFilename:name];
	    [self _updateGraphVals];
	    linkMgr = [[NXDataLinkManager allocFromZone:[self zone]] initWithDelegate:self fromFile:file];
	    [graph display];
	} else {
	    [self free];	/* couldn't load file */
	    return nil;
	}
    } else {
      /*
       * Create a new document.  We initialize it with a trivial expression
       * because that was easier than allowing the state where there is no
       * current expression.
       */
	[graph scaleToFit];
	[window setTitleAsFilename:[[[NXApp delegate] stringTable] valueForStringKey:"untitled doc"]];
	[resolutionSlider setIntValue:DEFAULT_RES];
	[resolutionText setIntValue:DEFAULT_RES];
	[expr setResolution:DEFAULT_RES];
	[equation setStringValue:"x"];
	[expr parse:"x"];
	setXRange(expr, [minXSlider floatValue], [maxXSlider floatValue]);
	[self _updateGraphVals];
	linkMgr = [[NXDataLinkManager allocFromZone:[self zone]] initWithDelegate:self];
	[graph scaleToFit];
	[graph display];
    }
    [window makeKeyAndOrderFront:self];
    return self;
}

- free {
    [expr free];
    NXZoneFree([self zone], name);
    NXZoneFree([self zone], realName);
    [linkMgr free];
    return [super free];
}

- windowDidResize:sender {
    NXRect frame;

  /*
   * Whenever the window changes size, we update the maximum resolution value
   * to be the width of the graph view.
   */
    [graph getFrame:&frame];
    [resolutionSlider setMaxValue:frame.size.width];
    return self;
}

- (const char *)filename {
    return name;
}

- (const char *)realFilename {
    return realName;
}

/*
 * This method is called whenever our window becomes the app's main window.
 * When this happens we get the 3D Panel and set its camera nil, so
 * the panel reflects the attributes of our window.
 */
- windowDidBecomeMain:sender {
    [[[NXApp delegate] threeDPanel] setCamera:nil];
    return self;
}

#define BUF_MAX		256

/*
 * This method is called when the user has just finished typing in an
 * expression.  This is where we validate that it is a legal expression.
 * If we dont like the expression, we return YES, which tells the text object
 * not to accept the user's entry.
 */
- (BOOL)textWillEnd:textObject {
    const char *var;
    BOOL parseError;
    char buffer[BUF_MAX];
    char *newText;
    int length;
    BOOL enableVariables[MAX_CONST - MIN_CONST + 1];
    int i;
    EXPEnumState state;	/* used to run through the vars of the expression */
    NXStringTable *stringTable;

  /* get the text out of the text object where the expression was typed */
    length = [textObject byteLength] + 1;
    if (length > BUF_MAX)
	newText = NXZoneMalloc(NXDefaultMallocZone(), sizeof(char) * length);
    else
	newText = buffer;
    [textObject getSubstring:newText start:0 length:length];

    if (*newText && (![expr text] || strcmp([expr text], newText))) {

      /* try parsing the text of the expression */
	parseError = ![expr parse:newText];
	if (!parseError) {
	    for (i = 0; i <= MAX_CONST - MIN_CONST; i++)
		enableVariables[i] = NO;

	  /*
	   * If it parsed successfully, make sure all the variables are single
	   * letters, and are either "x" or between "A" and "H".  As we
	   * find suitable variables, we remember then in the enableVariables
	   * array, so we can enable their controls later.
	   */
	    state = [expr beginVariableEnumeration];
	    while (var = [expr nextVariable:state])
		if (*var && !var[1] && *var >= MIN_CONST && *var <= MAX_CONST)
		    enableVariables[*var - MIN_CONST] = YES;
		else if (*var != 'x' || var[1])
		    parseError = YES;
	    [expr endVariableEnumeration:state];
	    if (!parseError) {
		for (i = 0; i <= MAX_CONST - MIN_CONST; i++)
		    [self _initVariable:i enable:enableVariables[i] value:1.0];
	      /* update the range of the x variable */
		setXRange(expr, [minXSlider floatValue], [maxXSlider floatValue]);
	    
	      /* update the graph object with current results and display it */
		[self _updateGraphVals];
		[graph scaleToFit];
		[graph display];
		[self _updateLinks];
	    }
	}
    } else if (!*newText) {
	parseError = YES;
    } else
	parseError = NO;
    if (length > BUF_MAX)
	NXZoneFree(NXDefaultMallocZone(), newText);

    if (parseError) {
	stringTable = [[NXApp delegate] stringTable];
	NXRunAlertPanel([stringTable valueForStringKey:"parse alert title"],
			[stringTable valueForStringKey:"parse alert message"],
			[stringTable valueForStringKey:"ok button"],
			NULL, NULL);
    }
    return parseError;
}

/*
 * This method is called after the user typed in a new expression.  The
 * expression has already been parsed in our textWillEnd: method.
 */
- equationChanged:sender {

  /* reselect the equation field so the user can type another */
    [equation selectText:self];
    return self;
}

/* called when either the minx or maxx slider changes */
- xRangeSliderChanged:sender {
    TextFieldCell *text;
    SliderCell *slider;
    NXEvent *event;

  /* update the associated text field */
    slider = [sender selectedCell];
    if (slider == maxXSlider)
	text = maxXText;
    else if (slider == minXSlider)
	text = minXText;
    else
	text = nil;
    NX_ASSERT(text, "Funny sender of xRangeSliderChanged: message");
    [text setFloatValue:[slider floatValue]];

  /* update the x range in the Expression and display the new graph */
    setXRange(expr, [minXText floatValue], [maxXText floatValue]);
    event = [NXApp currentEvent];
    [self _updateGraph:event->type == NX_LMOUSEUP];
    return self;
}

/* called when either the minx or maxx text changes */
- xRangeTextChanged:sender {
    TextFieldCell *text;
    SliderCell *slider;
    float val;

  /*
   * update the associated slider.  If the value typed is outside the current
   * range of the slider, we extend its range.
   */
    text = [sender selectedCell];
    if (text == maxXText)
	slider = maxXSlider;
    else if (text == minXText)
	slider = minXSlider;
    else
	slider = nil;
    NX_ASSERT(slider, "Funny sender of xRangeTextChanged: message");
    val = [text floatValue];
    [self _setSliderCell:slider value:val];

  /* update the x range in the Expression and display the new graph */
    setXRange(expr, [minXText floatValue], [maxXText floatValue]);
    [self _updateGraph:YES];
    [sender selectCell:text];
    return self;
}

/* called when one of the variables' sliders changes */
- variableSliderChanged:sender {
    int index;
    float val;
    char varName[2];
    NXEvent *event;

  /* update the associated text field */
    index = [variableSliders selectedRow];
    val = [[variableSliders selectedCell] floatValue];
    [[variableTexts cellAt:index :0] setFloatValue:val];
    varName[0] = MIN_CONST + index;
    varName[1] = '\0';

  /* update the variable's value in the Expression and display the new graph */
    [expr setVar:varName value:val];
    event = [NXApp currentEvent];
    [self _updateGraph:event->type == NX_LMOUSEUP];
    return self;
}

/* called when one of the variables' text fields changes */
- variableTextChanged:sender {
    int index;
    float val;
    char varName[2];
    TextFieldCell *text;
    SliderCell *slider;

  /* update the associated slider */
    text = [variableTexts selectedCell];
    index = [variableTexts selectedRow];
    slider = [variableSliders cellAt:index :0];
    val = [text floatValue];
    [self _setSliderCell:slider value:val];

  /* update the variable's value in the Expression and display the new graph */
    varName[0] = MIN_CONST + index;
    varName[1] = '\0';
    [expr setVar:varName value:val];
    [self _updateGraph:YES];
    [sender selectText:self];
    [sender selectCell:text];
    return self;
}

/*
 * The maximum allowable resolution.  The coordinates string of the userpath
 * that the LineGraph class uses to draw can only have 64K of data (like any
 * PostScript string). 64K of data is 16K of floats, or 8K coordinate pairs.
 * But every userpath has to have 4 numbers devoted to the bounding box, so
 * this reduces the number of allowable points to 8190.
 *
 * (In spite of all that nice math, 8190 doesn't seem to work, but 8189 does.
 * We need to look into this, but for now we don't push the limit.)
 */
#define MAX_RES	8189

/* called when either the resolution slider or text fields changes */
- resolutionChanged:sender {
    Cell *senderCell, *otherCell;
    int iVal;
    float fVal;
    NXEvent *event;

    if ([[sender cellAt:0 :0] isKindOfClassNamed:"SliderCell"]) {
	senderCell = resolutionSlider;
	otherCell = resolutionText;
    } else {
	senderCell = resolutionText;
	otherCell = resolutionSlider;
    }

    fVal = [senderCell floatValue];
    iVal = rint(fVal);
    if (iVal > MAX_RES)
	iVal = MAX_RES;
    [otherCell setIntValue:iVal];

  /* update the Expression's resolution and display the new graph */
    [expr setResolution:iVal];
    event = [NXApp currentEvent];
    [self _updateGraph:event->type != NX_LMOUSEDRAGGED];
    if (senderCell == resolutionText)
	[sender selectText:self];
    return self;
}

/* called when the zoom in button is pressed */
- zoomIn:sender {
    [autoScale setIntValue:0];
    [graph zoom:2.0];
    [graph display];
    [self _updateLinks];
    return self;
}

/* called when the zoom out button is pressed */
- zoomOut:sender {
    [autoScale setIntValue:0];
    [graph zoom:0.5];
    [graph display];
    [self _updateLinks];
    return self;
}

/* called when the auto scale switch changes */
- autoScale:sender {
    if ([autoScale intValue]) {
	[graph scaleToFit];	/* it got turned on, get scaled to fit */
	[graph display];
	[self _updateLinks];
    }
    return self;
}

/*
 * Copies the current view of the graph into the Pasteboard as PostScript. It
 * also writes a DataLink to the Pasteboard which can be used to create an
 * Object Link to the graph.  Since there is no notion of user selection
 * within a graph, its very easy for us to generate NXSelection objects - we
 * just use the standard Selection meaning "Select All". This is sent from the
 * Copy Graph menu item.
 */
- copyGraph:sender {
    Pasteboard *pb;
    const char *types[2];
    NXDataLink *link;

    pb = [Pasteboard new];
    types[0] = NXPostScriptPboardType;
    types[1] = NXDataLinkPboardType;
    [self _writeGraphToPasteboard:pb types:types num:2];
    link = [[NXDataLink alloc] initLinkedToSourceSelection:[NXSelection allSelection] managedBy:linkMgr supportingTypes:&NXPostScriptPboardType count:1];
    [link writeToPasteboard:pb];
    [link free];
    return self;
}

/*
 * Does the real work of copying the graph as PostScript.  This code is used
 * for copy/paste and Object Links support.
 *
 * Types must contain NXPostScriptPboardType.
 */
- _writeGraphToPasteboard:(Pasteboard *)pb types:(const char *const *)types num:(int)numTypes {
    NXStream *st;
    char *data;
    int dataLen, maxDataLen;

  /* Open a stream on memory where we will collect the PostScript */
    st = NXOpenMemory(NULL, 0, NX_WRITEONLY);

  /* Tell the Pasteboard we're going to copy PostScript */
    [pb declareTypes:types num:numTypes owner:nil];

  /* writes the PostScript for the whole graph as EPS into the stream */
    [graph copyPSCodeInside:NULL to:st];

  /* get the buffered up PostScript out of the stream */
    NXGetMemoryBuffer(st, &data, &dataLen, &maxDataLen);

  /* put the buffer in the Pasteboard, free the stream (and the buffer) */
    [pb writeType:NXPostScriptPboardType data:data length:dataLen];
    NXCloseMemory(st, NX_FREEBUFFER);
    return self;
}

/*** Object Links support - methods called by the DataLinkManager ***/

/* called by the DataLinkManager when new data is needed for a link */
- copyToPasteboard:(Pasteboard *)pboard at:(NXSelection *)selection cheapCopyAllowed:(BOOL)flag {
    NX_ASSERT([selection isEqual:[NXSelection allSelection]] || [selection isEqual:[NXSelection currentSelection]], "Funny selection passed to copyToPasteboard:at:");
    [self _writeGraphToPasteboard:pboard types:&NXPostScriptPboardType num:1];
    return self;
}

/* returns the window for the given selection */
- windowForSelection:(NXSelection *)selection {
    return window;		/* all our sels are always in our one window */
}

/*
 * We support continuously updating links by tracking changes to links to
 * us from open documents.  This is easy for Graph because all links can
 * only be to the whole graph, so any change in the graph is a change relevant
 * to any link.
 */
- (BOOL)dataLinkManagerTracksLinksIndividually:(NXDataLinkManager *)sender {
    return YES;
}

/*
 * Sent when we should start tracking a link.  We just keep all links we're
 * tracking in a list, and send them a message whenever the graph is changed.
 */
- dataLinkManager:(NXDataLinkManager *)sender startTrackingLink:(NXDataLink *)link {
    if (!linksTracked)
	linksTracked = [[List allocFromZone:[self zone]] init];
    [linksTracked addObject:link];
    return self;
}

/* Sent when we can forget a links we're tracking */
- dataLinkManager:(NXDataLinkManager *)sender stopTrackingLink:(NXDataLink *)link {
    [linksTracked removeObject:link];
    if ([linksTracked count] == 0) {
	[linksTracked free];
	linksTracked = nil;
    }
    return self;
}

/*
 * Called by other methods of GraphDoc whenever we make a change to the
 * document.  We tell the DataLinkManager about the change, and also notify
 * any links we are tracking.
 */
- _updateLinks {
    [linkMgr documentEdited];
    [linksTracked makeObjectsPerform:@selector(sourceEdited)];
    [window setDocEdited:YES];
    return self;
}

/* called when Save, Save As, or Save To is picked from the menu */
- save:sender   {  return [self _doSaveWithNewName:NO retainName:YES];  }
- saveAs:sender {  return [self _doSaveWithNewName:YES retainName:YES]; }
- saveTo:sender {  return [self _doSaveWithNewName:YES retainName:NO];  }

/*
 * All varieties of save go through this routine.  It covers all the cases
 * of running the Save Panel and retaining the name chosen.
 */
- _doSaveWithNewName:(BOOL)doNewName retainName:(BOOL)doRetain {
    SavePanel *savePanel;
    const char *saveName;	/* filename to save into */
    NXZone *zone;
    BOOL previouslySaved = (name != NULL);
    char realPath[MAXPATHLEN+1];

  /*
   * If the file is untitled or we are saving under a different name,
   * run the save panel to get a new name.
   */
    zone = [self zone];
    if (!name || doNewName) {
	savePanel = [SavePanel new];
	[savePanel setRequiredFileType:"xygraph"];
	if ([savePanel runModalForDirectory:NULL file:name]) {
	  /* if we want to keep this name, replace any old name */	
	    if (doRetain) {
    		NXZoneFree(zone, name);
		NXZoneFree(zone, realName);
		realName = NULL;
		name = NXCopyStringBufferFromZone([savePanel filename], zone);
	    }
	    saveName = [savePanel filename];
	} else
	    return nil;		/* user canceled */
    } else
      /* if we didn't run the Save Panel, save using the existing name */
	saveName = name;
    [self _write:saveName];
    if (!doRetain)
	[linkMgr documentSavedTo:saveName];
    else if (!previouslySaved || doNewName)
	[linkMgr documentSavedAs:saveName];
    else
	[linkMgr documentSaved];
    if (doRetain) {
	[window setDocEdited:NO];
	[window setTitleAsFilename:name];
	if (!realName && realpath(name, realPath)) {
	    realName = NXCopyStringBufferFromZone(realPath, [self zone]);
	}
    }
    return self;
}

- revertToSaved:sender {
    int response;
    NXStringTable *stringTable;
    const char *shortName;
    GraphDoc *newDoc;

    if ([window isDocEdited] && name) {
	stringTable = [[NXApp delegate] stringTable];
	shortName = strrchr(name, '/') + 1;
	response =  NXRunAlertPanel([stringTable valueForStringKey:"revert alert title"],
			[stringTable valueForStringKey:"revert alert message"],
			[stringTable valueForStringKey:"revert button"],
			[stringTable valueForStringKey:"cancel button"],
			NULL, shortName);
	if (response == NX_ALERTDEFAULT) {
	    [window setDelegate:nil];
	    [window close];
	    [NXApp delayedFree:self];
	    [linkMgr documentClosed];
	    [linkMgr free];
	    linkMgr = nil;
	    newDoc = [[GraphDoc allocFromZone:[self zone]] initFromFile:name];
	    if (!newDoc) {
		NXRunAlertPanel(
		    [stringTable valueForStringKey:"open alert title"],
		    [stringTable valueForStringKey:"open alert message"],
		    [stringTable valueForStringKey:"ok button"],
		    NULL, NULL, name);
	    }
	}
    }
    return self;
}

/* switches the colors of the graph and its background. */
- invertColors:sender {
    float bgGray;

    bgGray = [graph backgroundGray];
    [graph setBackgroundGray:[graph lineGray]];
    [graph setLineGray:bgGray];
    [graph display];		/* draw the new graph */
    return self;
}

/* Called when the window is closing.  We free the GraphDoc. */
- windowWillClose:sender {
    int response;
    NXStringTable *stringTable;
    const char *shortName;

    if ([window isDocEdited]) {
	stringTable = [[NXApp delegate] stringTable];
	if (name) {
	    shortName = strrchr(name, '/') + 1;
	} else {
	    shortName = [stringTable valueForStringKey:"untitled doc"];
	}
	response =  NXRunAlertPanel([stringTable valueForStringKey:"close alert title"],
			[stringTable valueForStringKey:"close alert message"],
			[stringTable valueForStringKey:"save button"],
			[stringTable valueForStringKey:"dont save button"],
			[stringTable valueForStringKey:"cancel button"],
			shortName);
	if (response != NX_ALERTDEFAULT && response != NX_ALERTALTERNATE) {
	    return nil;
	} else {
	    if (response == NX_ALERTDEFAULT && ![self save:sender])
		return nil;
	}
    }
    [NXApp delayedFree:self];
    [linkMgr documentClosed];
    return self;			/* says its OK to close */
}

/* Called whenever something changes to update the view of the graph. */
- _updateGraph:(BOOL)finalChange {
    [self _updateGraphVals];	/* update the values that we graph */
    if ([autoScale intValue])
	[graph scaleToFit];
    [graph display];		/* draw the new graph */
    if (finalChange)
	[self _updateLinks];
    return self;
}

/* Called whenever something changes to update the values that we graph. */
- _updateGraphVals {
    float *xVals, *yVals;
    int numXVals, numYVals;
    float minX, minY, maxX, maxY;

  /* extract the x values from the Expression */
    [expr varVector:"x" vector:&xVals numVals:&numXVals];
    [expr var:"x" min:&minX max:&maxX];

  /* extract the y values from the Expression.  This may cause a recalc. */
    [expr resultsVector:&yVals numVals:&numYVals];
    [expr resultsMin:&minY max:&maxY];

  /* set the points of the graph */
    [graph setPoints:[expr resolution] x:xVals y:yVals
				minX:minX minY:minY maxX:maxX maxY:maxY];
    return self;
}

/*
 * Writes a document to the given file using typedstreams.  We're very careful
 * about catching exceptions here so we can tell the user if his file didn't
 * get written out successfully.
 */
- _write:(const char *)filename {
    NXTypedStream *ts;
    NXRect graphViewRect;
    const char *stringVal;
    int intVal;
    float floatVal;
    char charVal;
    int rows, cols;
    int numVars;
    int i;
    NXStringTable *stringTable;
    volatile BOOL hadAnError = NO;
    char buffer[MAXPATHLEN+1];

  /* cons up the name of the backup file and remove it */
    strcpy(buffer, filename);
    strcat(buffer, "~");
    removeFile(buffer);

  /* move the existing file to the backup */
    rename(filename, buffer);

  /* create the new document as a file package (doc wrapper) */
    strcpy(buffer, filename);
    strcat(buffer, DOC_NAME);
    mkdir(filename, 0777);
    ts = NXOpenTypedStreamForFile(buffer, NX_WRITEONLY);
    if (ts) {
	NX_DURING
	    intVal = 1;			/* version of this write's data */
	    NXWriteType(ts, "i", &intVal);
	    stringVal = [expr text];
	    NXWriteType(ts, "*", &stringVal);
	    [graph getBounds:&graphViewRect];
	    NXWriteRect(ts, &graphViewRect);
	    [variableSliders getNumRows:&rows numCols:&cols];
	    numVars = 0;
	    for (i = 0; i < rows; i++) {
		if ([[variableSliders cellAt:i :0] isEnabled])
		    numVars++;
	    }
	    NXWriteType(ts, "i", &numVars);
	    for (i = 0; i < rows; i++)
		if ([[variableSliders cellAt:i :0] isEnabled]) {
		    charVal = MIN_CONST + i;
		    NXWriteType(ts, "c", &charVal);
		    writeCellFloat([variableSliders cellAt:i :0], ts);
		}
	    intVal = [expr resolution];
	    NXWriteType(ts, "i", &intVal);
	    writeCellFloat(minXSlider, ts);
	    writeCellFloat(maxXSlider, ts);
	    floatVal = [graph backgroundGray];
	    NXWriteType(ts, "f", &floatVal);
	    floatVal = [graph lineGray];
	    NXWriteType(ts, "f", &floatVal);
	    intVal = [autoScale intValue];
	    NXWriteType(ts, "i", &intVal);
	    NXCloseTypedStream(ts);
	NX_HANDLER
	    hadAnError = YES;
	    NX_DURING
		NXCloseTypedStream(ts);
	    NX_HANDLER
		/* ignore any error at this point */
	    NX_ENDHANDLER
	NX_ENDHANDLER
    } else 
	hadAnError = YES;
    if (hadAnError) {
	stringTable = [[NXApp delegate] stringTable];
	NXRunAlertPanel([stringTable valueForStringKey:"save alert title"],
			[stringTable valueForStringKey:"save alert message"],
			[stringTable valueForStringKey:"ok button"],
			NULL, NULL, filename);
	return nil;
    } else
	return self;
}

/* reads a document from the given file using typedstreams */
- _read:(const char *)filename {
    NXTypedStream *ts;
    NXRect graphViewRect;
    char *stringVal;
    int i;
    char varName;
    float floatVal1, floatVal2;
    int intVal;
    int numVars;
    BOOL parseError;

    ts = openDocStream(filename, NX_READONLY);
    if (ts) {
	NX_DURING
	    NXReadType(ts, "i", &intVal);	/* read file version */
	    NX_ASSERT(intVal == 1, "Unknown file version in -read");
	    NXReadType(ts, "*", &stringVal);
	    [equation setStringValue:stringVal];
	    parseError = ![expr parse:stringVal];
	    free(stringVal);
	    NX_ASSERT(!parseError, "Bad expression stored in data file");
	    if (parseError)
		NX_VALRETURN(nil);
	    NXReadRect(ts, &graphViewRect);
	    [graph setDrawSize:graphViewRect.size.width
			      :graphViewRect.size.height];
	    [graph setDrawOrigin:graphViewRect.origin.x
				:graphViewRect.origin.y];
	    NXReadType(ts, "i", &numVars);
	    for (i = 0; i < numVars; i++) {
		NXReadType(ts, "c", &varName);
		NXReadType(ts, "f", &floatVal1);
		[self _initVariable:varName - MIN_CONST enable:YES
							value:floatVal1];
	    }
	    NXReadType(ts, "i", &intVal);		/* read resolution */
	    [resolutionText setIntValue:intVal];
	    [self _setSliderCell:resolutionSlider value:intVal];
	    [expr setResolution:intVal];
	    NXReadType(ts, "f", &floatVal1);		/* read minX */
	    NXReadType(ts, "f", &floatVal2);		/* read maxX */
	    [minXText setFloatValue:floatVal1];
	    [self _setSliderCell:minXSlider value:floatVal1];
	    [maxXText setFloatValue:floatVal2];
	    [self _setSliderCell:maxXSlider value:floatVal2];
	    setXRange(expr, floatVal1, floatVal2);
	    NXReadType(ts, "f", &floatVal1);	/* read background gray */
	    [graph setBackgroundGray:floatVal1];
	    NXReadType(ts, "f", &floatVal1);		/* read line gray */
	    [graph setLineGray:floatVal1];
	    NXReadType(ts, "i", &intVal);		/* read auto scale */
	    [autoScale setIntValue:intVal];
	    NXCloseTypedStream(ts);
	  /* must use NX_VALRETURN to return from inside a NX_DURING */
	    NX_VALRETURN(self);
	NX_HANDLER
	    NX_DURING
		NXCloseTypedStream(ts);
	    NX_HANDLER
		/* ignore any error at this point */
	    NX_ENDHANDLER
	    return nil;
	NX_ENDHANDLER
    } else
	return nil;
}

/*
 * Inits a variable to either be on or off.  Used when we discover a new
 * set of variables after a parse, or to set up the variables from a document
 * that we read from a file.
 */
- (void)_initVariable:(int)index enable:(BOOL)flag value:(float)val {
    TextFieldCell *text;
    SliderCell *slider;
    char varName[2];

  /* set the initial value in the Expression */
    if (flag) {
	varName[0] = MIN_CONST + index;
	varName[1] = '\0';
	[expr setVar:varName value:val];
    }

  /* if the enabled status is changing... */
    if (flag != [[variableSliders cellAt:index :0] isEnabled])
	if (flag) {

	  /* enable all controls associated with the variable */
	    [[variableLabels cellAt:index :0] setTextGray:NX_BLACK];
	    slider = [variableSliders cellAt:index :0];
	    [slider setEnabled:YES];
	    [self _setSliderCell:slider value:val];
	    text = [variableTexts cellAt:index :0];
	    [text setEnabled:YES];
	    [text setFloatValue:val];
	} else if (!flag) {

	  /* disable all controls associated with the variable */
	    [[variableLabels cellAt:index :0] setTextGray:NX_DKGRAY];
	    slider = [variableSliders cellAt:index :0];
	    [slider setFloatValue:[slider minValue]];
	    [slider setEnabled:NO];
	    text = [variableTexts cellAt:index :0];
	    [text setStringValue:NULL];
	    [text setEnabled:NO];
	}
}

/* sets a slider to a value and adjust min/max */
- (void)_setSliderCell:(SliderCell *)slider value:(float)val {
    if (val < [slider minValue]) {
	[window disableDisplay];   /* so slider won't flash to new location */
	[slider setMinValue:val];
	[window reenableDisplay];
    } else if (val > [slider maxValue]) {
	[window disableDisplay];   /* so slider won't flash to new location */
	[slider setMaxValue:val];
	[window reenableDisplay];
    }
    [slider setFloatValue:val];
}

@end

/* little utility proc to write out the float value of a control */
static void writeCellFloat(Cell *obj, NXTypedStream *ts) {
    float val;

    val = [obj floatValue];
    NXWriteType(ts, "f", &val);
}


/*
 * A little utility proc to set the range of the x variable in the Expression.
 * It ensures that the min value isn't greater than the max value.
 */
static void setXRange(Expression *expr, float min, float max) {
    [expr setVar:"x" min:MIN(min, max) max:MAX(min, max)];
}

/* Opens a stream on the document regardless of whether its a doc wrapper. */
static NXTypedStream *openDocStream(const char *filename, int mode) {
    NXTypedStream *ts = NULL;
    struct stat statInfo;
    char buffer[MAXPATHLEN+1];

    if (stat(filename, &statInfo) == 0) {
	if (statInfo.st_mode & S_IFDIR) {
	    strcpy(buffer, filename);
	    strcat(buffer, DOC_NAME);
	    ts = NXOpenTypedStreamForFile(buffer, mode);
	} else {
	    ts = NXOpenTypedStreamForFile(filename, mode);
	}
    }
    return ts;
}

/* removes a directory, removing anything inside it.  Does not recurse */
static int removeFile(const char *file) {
    DIR *dirp;
    struct stat st;
    struct direct *dp;
    char *leaf = NULL;
    char path[MAXPATHLEN+1];

    if (!stat(file, &st)) {
	if ((st.st_mode & S_IFMT) == S_IFDIR) {
	    dirp = opendir(file);
	    for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
		if (strcmp(dp->d_name, ".") && strcmp(dp->d_name, "..")) {
		    if (!leaf) {
			strcpy(path, file);
			strcat(path, "/");
			leaf = path + strlen(path);
		    }
		    strcpy(leaf, dp->d_name);
		    if (unlink(path)) {
			closedir(dirp);
			return -1;
		    }
		}
	    }
	    return rmdir(file);
	} else {
	    return unlink(file);
	}
    }

    return -1;
}
